from django.conf import settings
from django.db import transaction
from django.db.models import F
from django.utils import timezone
from django.utils.crypto import constant_time_compare
from rest_framework import serializers

from sysreptor.pentests.models import (
    CollabEvent,
    CollabEventType,
    NoteType,
    ProjectNotebookPage,
    ShareInfo,
    UserNotebookPage,
)
from sysreptor.pentests.querysets import NotebookPageManagerBase
from sysreptor.users.serializers import RelatedUserSerializer
from sysreptor.utils.configuration import configuration


class ExcalidrawDataField(serializers.JSONField):
    def __init__(self, **kwargs):
        super().__init__(source='*', **kwargs)

    def to_representation(self, note):
        if note.type != NoteType.EXCALIDRAW:
            return None
        data = note.excalidraw_data
        if not data:
            return None
        return super().to_representation(data)

    def to_internal_value(self, value):
        if not value:
            return {}
        if not isinstance(value, dict) or not isinstance(value.get('elements'), list):
            raise serializers.ValidationError('Invalid excalidraw data')
        return {self.field_name: super().to_internal_value(value)}


class NotebookPageSerializerBase(serializers.ModelSerializer):
    id = serializers.UUIDField(source='note_id', read_only=True)
    title = serializers.CharField(required=False, allow_blank=True)
    text = serializers.CharField(required=False, allow_blank=True, trim_whitespace=False)
    parent = serializers.UUIDField(source='parent.note_id', allow_null=True, required=False)
    excalidraw_data = ExcalidrawDataField(required=False, allow_null=True, write_only=True)

    class Meta:
        fields = [
            'id', 'created', 'updated',
            'type', 'title', 'text', 'checked', 'icon_emoji',
            'order', 'parent', 'excalidraw_data',
        ]
        extra_kwargs = {
            'parent': {'read_only': True},
            'order': {'read_only': True},
        }

    def validate_excalidraw_data(self, value):
        if not value:
            return None
        if (self.instance and self.instance.type != NoteType.EXCALIDRAW) or \
           (not self.instance and self.initial_data.get('type') != NoteType.EXCALIDRAW):
            raise serializers.ValidationError('note.type is not excalidraw')
        return value

    @transaction.atomic()
    def update(self, instance, validated_data):
        validated_data.pop('parent', None)
        excalidraw_data = validated_data.pop('excalidraw_data', None)
        if instance.type != NoteType.TEXT:
            validated_data.pop('text', None)
        out = super().update(instance, validated_data)
        if excalidraw_data:
            instance.update_excalidraw_data(excalidraw_data)
        return out


class ProjectNotebookPageSerializer(NotebookPageSerializerBase, serializers.ModelSerializer):
    assignee = RelatedUserSerializer(required=False, allow_null=True)
    is_shared = serializers.SerializerMethodField()

    class Meta(NotebookPageSerializerBase.Meta):
        model = ProjectNotebookPage
        fields = NotebookPageSerializerBase.Meta.fields + ['assignee', 'is_shared']

    def get_is_shared(self, obj) -> bool:
        if (is_shared := getattr(obj, 'is_shared', None)) is not None:
            return is_shared
        return obj.shareinfos.only_active().exists()


class UserNotebookPageSerializer(NotebookPageSerializerBase, serializers.ModelSerializer):
    class Meta(NotebookPageSerializerBase.Meta):
        model = UserNotebookPage


class NotebookPageCreateSerializerMixin:
    def get_extra_kwargs(self):
        return super().get_extra_kwargs() | {
            'parent': {'read_only': False, 'required': False},
            'order': {'read_only': False, 'required': False, 'allow_null': True},
            'type': {'read_only': False, 'required': False},
        }

    def get_notebook_object(self):
        return None

    def validate_parent(self, value):
        if value:
            parent = self.Meta.model.objects \
                .filter(**self.get_notebook_object()) \
                .filter(note_id=value) \
                .first()
            if not parent:
                raise serializers.ValidationError('Invalid note id')
            return parent
        return value

    @transaction.atomic()
    def create(self, validated_data):
        validated_data['parent'] = validated_data.get('parent', {}).get('note_id')
        excalidraw_data = validated_data.pop('excalidraw_data', None)

        if validated_data.get('order'):
            self.Meta.model.objects \
                .filter(**self.get_notebook_object()) \
                .filter(parent=validated_data.get('parent')) \
                .filter(order__gte=validated_data.get('order')) \
                .update(order=F('order') + 1)
        else:
            validated_data.pop('order', None)

        instance = super().create(validated_data | self.get_notebook_object())
        if excalidraw_data:
            instance.update_excalidraw_data(excalidraw_data)
        return instance


class ProjectNotebookPageCreateSerializer(NotebookPageCreateSerializerMixin, ProjectNotebookPageSerializer):
    def get_notebook_object(self):
        return {'project': self.context['project']}


class UserNotebookPageCreateSerializer(NotebookPageCreateSerializerMixin, UserNotebookPageSerializer):
    def get_notebook_object(self):
        return {'user': self.context['user']}


class NotebookPageSortSerializerBase(serializers.ModelSerializer):
    id = serializers.UUIDField(source='note_id')
    parent = serializers.UUIDField(source='parent.note_id', allow_null=True)

    class Meta:
        fields = ['id', 'parent', 'order']

    def validate_id(self, value):
        if not next(filter(lambda n: n.note_id == value, self.parent.instance), None):
            raise serializers.ValidationError('Invalid note id')
        return value

    def validate_parent(self, value):
        parent = next(filter(lambda n: n.note_id == value, self.parent.instance), None)
        if value is not None and not parent:
            raise serializers.ValidationError('Invalid note id')
        return parent


class ProjectNotebookPageSortSerializer(NotebookPageSortSerializerBase, serializers.ModelSerializer):
    class Meta(NotebookPageSortSerializerBase.Meta):
        model = ProjectNotebookPage


class UserNotebookPageSortSerializer(NotebookPageSortSerializerBase, serializers.ModelSerializer):
    class Meta(NotebookPageSortSerializerBase.Meta):
        model = UserNotebookPage


class NotebookPageSortListSerializerBase(serializers.ListSerializer):
    def get_related_id():
        pass

    def _send_collab_event(self, event: CollabEvent):
        raise NotImplementedError()

    def send_collab_event(self, instances):
        time = max([n.updated for n in instances] or [timezone.now()])
        self._send_collab_event(CollabEvent.objects.create(
            related_id=self.get_related_id(),
            type=CollabEventType.SORT,
            path='notes',
            created=time,
            version=time.timestamp(),
            data={
                'sort': self.__class__(instances).data,
            },
        ))

    def update(self, instance, validated_data):
        # Update values
        missing_notes = []
        for note in instance:
            if data := next(filter(lambda d: note.note_id == d.get('note_id'), validated_data), None):
                note.parent = data.get('parent', {}).get('note_id')
                note.order = data.get('order')
                note.updated = timezone.now()
            else:
                missing_notes.append(note)

        self.child.Meta.model.objects.check_parent_and_order(instance, missing_notes)
        self.child.Meta.model.objects.bulk_update(instance, ['parent_id', 'order', 'updated'])
        self.send_collab_event(instance)
        return instance


class ProjectNotebookPageSortListSerializer(NotebookPageSortListSerializerBase):
    child = ProjectNotebookPageSortSerializer()

    def get_related_id(self):
        return self.context['project'].id

    def _send_collab_event(self, event: CollabEvent):
        from sysreptor.pentests.consumers import send_collab_event_project
        send_collab_event_project(event)


class UserNotebookPageSortListSerializer(NotebookPageSortListSerializerBase):
    child = UserNotebookPageSortSerializer()

    def get_related_id(self):
        return self.context['user'].id

    def _send_collab_event(self, event: CollabEvent):
        from sysreptor.pentests.consumers import send_collab_event_user
        send_collab_event_user(event)


class RelatedNoteIdSerializerField(serializers.ListField):
    child = serializers.UUIDField()
    allow_empty = False

    def to_internal_value(self, value):
        value = super().to_internal_value(value)

        parent_model = self.context.get('project') or self.context.get('user')
        all_notes = NotebookPageManagerBase().to_ordered_list_flat(parent_model.notes.all())
        notes = [n for n in all_notes if n.note_id in value]
        if len(notes) != len(set(value)):
            raise serializers.ValidationError(f'Unknown IDs: {", ".join(set(map(str, value)) - set(map(lambda n: str(n.note_id), notes)))}')
        return notes


class ExportNotesOptionsSerializer(serializers.Serializer):
    notes = RelatedNoteIdSerializerField(required=False)


class ExportPdfOptionsSerializer(serializers.Serializer):
    pass


class ExportPdfMultipleOptionsSerializer(serializers.Serializer):
    notes = RelatedNoteIdSerializerField(required=True)


class ShareInfoSerializer(serializers.ModelSerializer):
    shared_by = RelatedUserSerializer(required=False, allow_null=True, read_only=True)

    class Meta:
        model = ShareInfo
        fields = [
            'id', 'created', 'updated', 'shared_by',
            'expire_date', 'is_revoked', 'password', 'permissions_write',
            'comment',
        ]

    # def validate_expire_date(self, value):
    #     if timezone.now().date() > value:
    #         raise serializers.ValidationError('Expire date cannot be in the past')
    #     return value

    def validate_password(self, value):
        if not value and configuration.SHARING_PASSWORD_REQUIRED:
            raise serializers.ValidationError('Password required')
        return value

    def validate_permissions_write(self, value):
        if value and configuration.SHARING_READONLY_REQUIRED:
            raise serializers.ValidationError('Writeable shares are not allowed')
        return value

    def create(self, validated_data):
        return super().create(validated_data | {
            'note': self.context['note'],
            'shared_by': self.context['request'].user,
        })


class ShareInfoPublicSerializer(serializers.ModelSerializer):
    password_required = serializers.SerializerMethodField()
    password_verified = serializers.SerializerMethodField()
    permissions_write = serializers.SerializerMethodField()
    note_id = serializers.UUIDField(source='note.note_id')

    class Meta:
        model = ShareInfo
        fields = [
            'id', 'created', 'updated',
            'expire_date', 'permissions_write',
            'password_required', 'password_verified',
            'note_id',
        ]

    def get_password_required(self, obj) -> bool:
        return bool(obj.password)

    def get_password_verified(self, obj) -> bool:
        if not obj.password:
            return True
        return str(obj.id) in self.context['request'].session.get('authorized_shareids', [])

    def get_permissions_write(self, obj) -> bool:
        return obj.permissions_write and not obj.note.project.readonly


class ShareInfoCheckPasswordSerializer(serializers.ModelSerializer):
    class Meta:
        model = ShareInfo
        fields = ['password']
        extra_kwargs = {
            'password': {'write_only': True, 'required': True, 'allow_null': False},
        }

    def validate_password(self, value):
        if not self.instance.password:
            raise serializers.ValidationError('No password set')
        if not constant_time_compare(value, self.instance.password):
            # Increment failed PW counter
            ShareInfo.objects.filter(pk=self.instance.pk).increment_failed_password_attempts()
            self.instance.refresh_from_db()
            # Revoke share if too many failed attempts => brute force protection
            if self.instance.failed_password_attempts >= settings.SHARING_MAX_FAILED_PASSWORD_ATTEMPTS:
                self.instance.is_revoked = True
                self.instance.save()
            raise serializers.ValidationError('Invalid password')
        return value


class ProjectNotebookPagePublicSerializer(NotebookPageSerializerBase, serializers.ModelSerializer):
    class Meta(NotebookPageSerializerBase.Meta):
        model = ProjectNotebookPage


class ProjectNotebookPageCreatePublicSerializer(ProjectNotebookPageCreateSerializer):
    def get_extra_kwargs(self):
        return super().get_extra_kwargs() | {
            'parent': {'read_only': False, 'required': True, 'allow_null': False},
        }

    def get_notebook_object(self):
        return {'project': self.context['share_info'].note.project}

    def validate_parent(self, value):
        parent = ProjectNotebookPage.objects \
            .child_notes_of(self.context['share_info'].note) \
            .filter(note_id=value) \
            .first()
        if not parent:
             raise serializers.ValidationError('Invalid note id')
        return parent


class NoteExcalidrawDataSerializer(serializers.Serializer):
    elements = serializers.SerializerMethodField()

    class Meta:
        fields = ['elements']

    def get_elements(self, obj):
        return obj.excalidraw_data.get('elements', [])

